/*
  Firmata->h - Firmata library v2.5.8 - 2018-04-15
  Copyright (c) 2006-2008 Hans-Christoph Steiner.  All rights reserved.
  Copyright (C) 2009-2017 Jeff Hoefs.  All rights reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  See file LICENSE.txt for further informations on licensing terms.
*/

#ifndef Firmata_h
#define Firmata_h

#include <nStream.h>
#include "Boards.h" /* Hardware Abstraction Layer + Wiring/Arduino */
#include "FirmataDefines.h"
#include "FirmataMarshaller.h"
#include "FirmataParser.h"

#ifndef ARDUINO
typedef unsigned char byte;
#define TOTAL_PINS 100
#endif
/* DEPRECATED as of Firmata v2.5.1. As of 2.5.1 there are separate version numbers for
 * the protocol version and the firmware version.
 */
#define FIRMATA_MAJOR_VERSION 2  // same as FIRMATA_PROTOCOL_MAJOR_VERSION
#define FIRMATA_MINOR_VERSION 5  // same as FIRMATA_PROTOCOL_MINOR_VERSION
#define FIRMATA_BUGFIX_VERSION 1 // same as FIRMATA_PROTOCOL_BUGFIX_VERSION

// extended command set using sysex (0-127/0x00-0x7F)
/* 0x00-0x0F reserved for user-defined commands */
// these are DEPRECATED to make the naming more consistent
#define FIRMATA_STRING 0x71          // same as STRING_DATA
#define SYSEX_I2C_REQUEST 0x76       // same as I2C_REQUEST
#define SYSEX_I2C_REPLY 0x77         // same as I2C_REPLY
#define SYSEX_SAMPLING_INTERVAL 0x7A // same as SAMPLING_INTERVAL

// pin modes
//#define INPUT                 0x00 // defined in Arduino.h
//#define OUTPUT                0x01 // defined in Arduino.h
// DEPRECATED as of Firmata v2.5
#define ANALOG 0x02  // same as PIN_MODE_ANALOG
#define PWM 0x03     // same as PIN_MODE_PWM
#define SERVO 0x04   // same as PIN_MODE_SERVO
#define SHIFT 0x05   // same as PIN_MODE_SHIFT
#define I2C 0x06     // same as PIN_MODE_I2C
#define ONEWIRE 0x07 // same as PIN_MODE_ONEWIRE
#define STEPPER 0x08 // same as PIN_MODE_STEPPER
#define ENCODER 0x09 // same as PIN_MODE_ENCODER
#define IGNORE 0x7F  // same as PIN_MODE_IGNORE

namespace firmata {

    class FirmataClass {
    public:
        typedef void (*callbackFunction)(FirmataClass *, nStream *, uint8_t, int);

        typedef void (*systemCallbackFunction)(FirmataClass *, nStream *);

        typedef void (*stringCallbackFunction)(FirmataClass *, nStream *, char *);

        typedef void (*sysexCallbackFunction)(FirmataClass *, nStream *, uint8_t command, uint16_t argc, uint8_t *argv);

        FirmataClass();

        virtual ~FirmataClass() = default;

        /* Arduino constructors */
        void begin();

        /* querying functions */
        void printVersion(nStream *FirmataStream);

        void blinkVersion();

        void printFirmwareVersion(nStream *FirmataStream);

        // void setFirmwareVersion(byte major, byte minor);  // see macro below
        void setFirmwareNameAndVersion(const char *name, byte major, byte minor);

        void disableBlinkVersion();

        /* serial receive handling */
        int available(nStream *FirmataStream);

        void processInput(nStream *FirmataStream);

        void parse(nStream *, unsigned char value);

        bool isParsingMessage();

        virtual /* serial send handling */
        void
        sendAnalog(nStream *, byte pin, int value);

        virtual void sendDigital(nStream *, byte pin, int value);

        void sendDigitalPort(nStream *, byte portNumber, int portData);

        void sendString(nStream *, const char *string);

        void sendString(nStream *, byte command, const char *string);

        void sendSysex(nStream *, byte command, uint16_t bytec, byte *bytev);

        void write(nStream *FirmataStream, byte c);

        int writeBytes(nStream *FirmataStream, byte *ca, uint8_t len);

        /* attach & detach callback functions to messages */
        void attach(uint8_t command, callbackFunction newFunction);

        void attach(uint8_t command, systemCallbackFunction newFunction);

        void attach(uint8_t command, stringCallbackFunction newFunction);

        void attach(uint8_t command, sysexCallbackFunction newFunction);

        void detach(uint8_t command);
        /* access pin state and config */
        byte getPinMode(byte pin);

        void setPinMode(byte pin, byte config);

        virtual /* access pin state */
        int getPinState(byte pin);

        virtual void setPinState(byte pin, int state);

        /* utility methods */
        void sendValueAsTwo7bitBytes(nStream *FirmataStream, int value);

        void startSysex(nStream *FirmataStream);

        void endSysex(nStream *FirmataStream);

        void encodeByteStream(nStream *FirmataStream, size_t, uint8_t *);

        void flush(nStream *FirmataStream);

        FirmataParser parser;
        uint8_t parserBuffer[FIRMATA_BUFFER_SZ]{};
        FirmataMarshaller marshaller;
        //        nStream *FirmataStream{};

        /* firmware name and version */
        byte firmwareVersionCount = 0;
        byte *firmwareVersionVector = nullptr;

        /* pin configuration */
//        byte pinConfig[TOTAL_PINS]{};
//        int pinState[TOTAL_PINS]{};
        bool blinkVersionDisabled{};

        /* private methods ------------------------------ */
        void strobeBlinkPin(byte pin, int count, int onInterval, int offInterval);

        friend void FirmataMarshaller::encodeByteStream(nStream *FirmataStream, size_t bytec, uint8_t *bytev,
                                                        size_t max_bytes) ;

        /* callback functions */
        callbackFunction currentAnalogCallback = 0;
        callbackFunction currentDigitalCallback;
        callbackFunction currentPinModeCallback;
        callbackFunction currentPinValueCallback;
        callbackFunction currentReportAnalogCallback;
        callbackFunction currentReportDigitalCallback;
        stringCallbackFunction currentStringCallback;
        sysexCallbackFunction currentSysexCallback;
        systemCallbackFunction currentSystemResetCallback;

        /* static callbacks */
        inline static void
        staticAnalogCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, uint16_t value) {
            if (Firmata->currentAnalogCallback) {
                Firmata->currentAnalogCallback(Firmata, FirmataStream, command, (int) value);
            }
        }

        inline static void
        staticDigitalCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, uint16_t value) {
            if (Firmata->currentDigitalCallback) {
                Firmata->currentDigitalCallback(Firmata, FirmataStream, command, (int) value);
            }
        }

        inline static void
        staticPinModeCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, uint16_t value) {
            if (Firmata->currentPinModeCallback) {
                Firmata->currentPinModeCallback(Firmata, FirmataStream, command, (int) value);
            }
        }

        inline static void
        staticPinValueCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, uint16_t value) {
            if (Firmata->currentPinValueCallback) {
                Firmata->currentPinValueCallback(Firmata, FirmataStream, command, (int) value);
            }
        }

        inline static void
        staticReportAnalogCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, uint16_t value) {
            if (Firmata->currentReportAnalogCallback) {
                Firmata->currentReportAnalogCallback(Firmata, FirmataStream, command, (int) value);
            }
        }

        inline static void
        staticReportDigitalCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, uint16_t value) {
            if (Firmata->currentReportDigitalCallback) {
                Firmata->currentReportDigitalCallback(Firmata, FirmataStream, command, (int) value);
            }
        }

        inline static void staticStringCallback(FirmataClass *Firmata, nStream *FirmataStream, const char *c_str) {
            if (Firmata->currentStringCallback) {
                Firmata->currentStringCallback(Firmata, FirmataStream, (char *) c_str);
            }
        }

        inline static void
        staticSysexCallback(FirmataClass *Firmata, nStream *FirmataStream, uint8_t command, size_t argc,
                            uint8_t *argv) {
            if (Firmata->currentSysexCallback) {
                Firmata->currentSysexCallback(Firmata, FirmataStream, command, (uint16_t) argc, argv);
            }
        }

        inline static void
        staticReportFirmwareCallback(FirmataClass *context, nStream *FirmataStream, size_t, size_t, const char *) {
            context->printFirmwareVersion(FirmataStream);
        }

        inline static void staticReportVersionCallback(FirmataClass *context, nStream *FirmataStream) {

            context->printVersion(FirmataStream);
        }

        inline static void staticSystemResetCallback(FirmataClass *Firmata, nStream *FirmataStream) {
            if (Firmata->currentSystemResetCallback) {
                Firmata->currentSystemResetCallback(Firmata, FirmataStream);
            }
        }
    };

} // namespace firmata

extern "C"
{
// callback function types
typedef firmata::FirmataClass::callbackFunction callbackFunction;
typedef firmata::FirmataClass::systemCallbackFunction systemCallbackFunction;
typedef firmata::FirmataClass::stringCallbackFunction stringCallbackFunction;
typedef firmata::FirmataClass::sysexCallbackFunction sysexCallbackFunction;
}

extern firmata::FirmataClass Firmata;

/*==============================================================================
 * MACROS
 *============================================================================*/

/* shortcut for setFirmwareNameAndVersion() that uses __FILE__ to set the
 * firmware name.  It needs to be a macro so that __FILE__ is included in the
 * firmware source file rather than the library source file.
 */
#define setFirmwareVersion(x, y) setFirmwareNameAndVersion(__FILE__, x, y)

int decodeByteStream(size_t bytec, const byte *bytev, byte *buf);

#endif /* Firmata_h */
